﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using FarseerGames.FarseerPhysics;
using FarseerGames.FarseerPhysics.Collisions;
using FarseerGames.FarseerPhysics.Dynamics;
using FarseerGames.FarseerPhysics.Factories;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Content.Pipeline.Serialization.Compiler;
using Microsoft.Xna.Framework.Graphics;
using System.Linq;

namespace FolderData
{
    public enum FoldState
    {
        FOLDREADY,
        FOLDANCHORED,
        FOLDING,
        FOLDED,
        UNFOLDING,
        UNFOLDED
    }

    public enum CheckPointState
    {
        Init,
        Teleporting,
        None,
    }

    #region Boundary Class

    public static class Boundary
    {
        static Body boundary;
        static Geom[] geoms = new Geom[4];

        /// <summary>
        /// 플레이어가 맵 밖으로 나가지 못하도록 하는 적절한 외각선을 추가한다.
        /// </summary>
        public static void Initialize(Rectangle MapSize)
        {
            boundary = new Body();
            boundary.IsStatic = true;
            geoms[0] = GeomFactory.Instance.CreateRectangleGeom(boundary, 20, MapSize.Height, new Vector2(-10, MapSize.Height / 2), .0f);
            geoms[1] = GeomFactory.Instance.CreateRectangleGeom(boundary, 20, MapSize.Height, new Vector2(MapSize.Width + 10, MapSize.Height / 2), .0f);
            geoms[2] = GeomFactory.Instance.CreateRectangleGeom(boundary, MapSize.Width, 20, new Vector2(MapSize.Width / 2, -10), .0f);
            geoms[3] = GeomFactory.Instance.CreateRectangleGeom(boundary, MapSize.Width, 20, new Vector2(MapSize.Width / 2, MapSize.Height + 10), .0f);

            boundary.Position = new Vector2(MapSize.X, MapSize.Y) - Camera.ScreenCenter;
        }

        public static void AddBoundaries(PhysicsSimulator Simulator)
        {
            Simulator.Add(boundary);
            foreach (var g in geoms)
                Simulator.Add(g);
        }

        public static void RemoveBoundaries(PhysicsSimulator Simulator)
        {
            Simulator.Remove(boundary);
            foreach (var g in geoms)
                Simulator.Remove(g);
        }
    }

    #endregion
    
    public class Map : ICloneable
    {
        #region Fields
        //Map의 Field에 무언가 추가할 경우 다음의 순서에 따라 작업하세요.
        //1. Field에 추가합니다.
        //2. Clone여부를 따져서 Clone함수에서 적절히 클론하도록 합니다.
        //3. Bind로 데이터를 생성해야 하는지 따져서 BindDatas(), ReloadDatas()함수를 적절히 수정합니다.
        //4. Serializable 여부를 따져서 Processor쪽에 해당 Field의 Serialize / Deserialize를 추가해줍니다.
        //5. 만약 초기화가 필요하다면 생성자에 추가하세요.
        private Stack<PhysicsSimulator> _stateStack = new Stack<PhysicsSimulator>();

        private Stack<Dictionary<String, Substance>> _substancesStack = new Stack<Dictionary<String, Substance>>();
        private Camera _camera;
        private Character _player;

        protected string _bgAssetName;
        protected MapBackground _background;

        private bool _isSaved; //맵에디터에서 사용. 맵 내용 업데이트 이후 저장되었는가?

        #endregion

        #region Properties

        public Vector2 StartPos
        {
            get { return _playerDataSet.Position; }
            set { _playerDataSet.Position = value; }
        }

        /// <summary>
        /// 맵의 크기를 저장합니다.
        /// 맵 배경의 경우 MapSize의 x, y 좌표에 뿌려지게 됩니다. 
        /// </summary>
        public Rectangle MapSize
        {
            get { return _camera.Viewsize; }
            set {
                Boundary.RemoveBoundaries(Simulator);
                _camera.Viewsize = value; 
                Boundary.Initialize(value);
                Boundary.AddBoundaries(Simulator);
            }
        }

        public string BgAssetName
        {
            get { return _bgAssetName; }
            set { _bgAssetName = value; }
        }

        #region ContentSerializerIgnores

        [ContentSerializerIgnore]
        public Camera Camera { get { return _camera; } }
        /// <summary>
        /// Map의 물리계를 담당하는 물리엔진
        /// </summary>
        [ContentSerializerIgnore]
        public PhysicsSimulator Simulator { get { return _stateStack.Peek(); } }

        [ContentSerializerIgnore]
        public Stack<PhysicsSimulator> StateStack
        {
            get { return _stateStack; }
            set { _stateStack = value; }
        }
        /// <summary>
        /// Map의 주인공 오브젝트.
        /// LoadContent를 한 이후에 설정해 주어야 한다.
        /// </summary>
        [ContentSerializerIgnore]
        public Character Player
        {
            get { return _player; }
            set { _player = value; }
        }
        /// <summary>
        /// 맵을 에디트 하고 있는가.
        /// </summary>
        [ContentSerializerIgnore]
        public bool IsMapEditing;

        /// <summary>
        /// 맵이 저장되었는가?
        /// </summary>
        [ContentSerializerIgnore]
        [Browsable(false)]
        public bool IsSaved
        {
            get { return _isSaved; }
            set { _isSaved = value; }
        }

        [ContentSerializerIgnore]
        public MapBackground Background
        {
            get { return _background; }
            set { _background = value;}
        }
        [ContentSerializerIgnore]
        public Vector2 BgPosition
        {
            get { return new Vector2(MapSize.X, MapSize.Y); }
        }
        [ContentSerializerIgnore]
        public bool IsFolded = false;

        [ContentSerializerIgnore]
        public Vector2 foldingCameraPosition;

        #endregion
        
        #endregion

        #region Initialize

        public Map()
        {
            PhysicsSimulator _simulator = new PhysicsSimulator(new Vector2(0, 800));
            _simulator.FrictionType = FrictionType.Average;

            _stateStack.Push(_simulator);

            var _substances = new Dictionary<String, Substance>();
            _substancesStack = new Stack<Dictionary<String, Substance>>();
            _substancesStack.Push(_substances);
           
            _characterDatas = new List<Character.CharacterDataSet>();
            _itemDatas = new List<Item.ItemDataSet>();
            _terrainDatas = new List<Terrain.TerrainDataSet>();
            IsMapEditing = false;

            _camera = new Camera(new Vector2(100, 100), new Rectangle(0, 0, 2400, 2400));
            _isSaved = false;
            _playerDataSet = new Character.CharacterDataSet();
            _playerDataSet.Name = "black_cat"; //dummy
            _playerDataSet.id = "player";
            _monsterBlockPositions = new List<Vector2>();
            _background = null;
            _bgAssetName = string.Empty;
            
            Boundary.Initialize(MapSize);
            Boundary.AddBoundaries(Simulator);
        }

        public void LoadContent(ContentManager content)
        {
            _background = content.Load<MapBackground>(BgAssetName);
            _background.LoadContent(content);
        }

        #endregion

        #region Methods
        
        /// <summary>
        /// 이 맵에 해당 물체를 등록합니다.
        /// </summary>
        /// <param name="s">등록할 물체</param>
        public bool AddSubstance(Substance s, String id)
        {
            if(Substances.ContainsKey(id))
                return false;

            Substances.Add(id, s);
            return true;
        }

        public void RemoveSubstance(Substance s, String id)
        {
            Substances.Remove(id);
        }

        #endregion

        #region Update and Draw

        public void Update(GameTime gameTime, FoldState state)
        {
            if(state == FoldState.FOLDED || state == FoldState.UNFOLDED)
                Simulator.Update(gameTime.ElapsedGameTime.Milliseconds * 0.001f);

            foreach (var s in Substances.Values)
            {
                //강제로 물체들의 회전을 방지한다.
                s.Body.AngularVelocity = 0f;
                if (!s.Body.IsStatic)
                    s.Body.Rotation = 0f;
                s.Update(gameTime);
            }

            //camera update.
            if (IsMapEditing || _player == null)
                _camera.Update(null, gameTime);
            else
                _camera.Update(_player.Position, gameTime);
        }

        public void DrawCharacters(SpriteBatch spriteBatch, Color color)
        {
            var query =
                from s in this.Substances.Values
                where s.GetType() == typeof(Character) || s.IsFoldable == false
                orderby s.Layer descending
                select s;

            DrawQuery(spriteBatch, color, query);
        }

        public void DrawFoldables(SpriteBatch spriteBatch, Color color)
        {
            var query =
                from i in this.Substances.Values
                where (i.GetType() != typeof(Character) && i.IsFoldable == true && !i.IsUnfoldBehavior)
                orderby i.Layer descending
                select i;

            DrawQuery(spriteBatch, color, query);
        }

        public void DrawUnfoldables(SpriteBatch spriteBatch, Color color)
        {
            var query =
                from i in this.Substances.Values
                where (i.GetType() != typeof(Character) && i.IsFoldable == false && !i.IsUnfoldBehavior)
                orderby i.Layer descending
                select i;

            DrawQuery(spriteBatch, color, query);
        }

        public void DrawShift(SpriteBatch spriteBatch, Color color)
        {
            var query =
                from i in this.Substances.Values
                where i.IsUnfoldBehavior == true
                orderby i.Layer descending
                select i;

            DrawQuery(spriteBatch, color, query);
        }

        private void DrawQuery(SpriteBatch spriteBatch, Color color, IOrderedEnumerable<Substance> query)
        {
            foreach (var s in query)
            {
                //그릴 대상의 좌표를 카메라 상대좌표로 바꿈
                Vector2 pos = new Vector2(Camera.SCREEN_WIDTH / 2f + s.Position.X - _camera.Position.X,
                    Camera.SCREEN_HEIGHT / 2f + s.Position.Y - _camera.Position.Y);

                //대상이 그려질 수 있는 범위에 있다면
                //임시 패치. 텍스쳐가 없을 경우 그리지 않도록 한다.
                if (s.Texture != null && (-s.Texture.Width < pos.X && pos.X < Camera.SCREEN_WIDTH + s.Texture.Width &&
                    -s.Texture.Height < pos.Y && pos.Y < Camera.SCREEN_HEIGHT + s.Texture.Height))
                {
                    Color drawColor = new Color(Vector4.Multiply(color.ToVector4(), s.ColorMod.ToVector4()));
                    spriteBatch.Draw(s.Texture, pos, s.Rect, drawColor, s.Rotation, s.Origin, 1f, SpriteEffects.None, 0f);
                }
            }
        }

        #endregion

        #region Map Data and Procedures about Map Loading & Editing

        //Fields
        //시작 좌표
        //AssetNames of Characters, Terrains, Items
        //Additional Geoms
        //(Optional) Camerawork
        //(Optional) The size of folding frame.
        protected string _name;
        protected List<Character.CharacterDataSet> _characterDatas;
        protected List<Terrain.TerrainDataSet> _terrainDatas;
        protected List<Item.ItemDataSet> _itemDatas;
        protected Character.CharacterDataSet _playerDataSet;
        protected List<Vector2> _monsterBlockPositions;                 //몬스터 블록의 위치
        public readonly Point MonsterBlockSize = new Point(50, 50);     //몬스터를 가로막는 가상 벽의 사이즈.
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }
        public List<Character.CharacterDataSet> CharacterDatas
        {
            get { return _characterDatas; }
            set { _characterDatas = value; }
        }
        public List<Terrain.TerrainDataSet> TerrainDatas
        {
            get { return _terrainDatas; }
            set { _terrainDatas = value; }
        }
        public List<Item.ItemDataSet> ItemDatas
        {
            get { return _itemDatas; }
            set { _itemDatas = value; }
        }

        /// <summary>
        /// 맵의 Player에 해당하는 AssetName
        /// </summary>
        public string PlayerAssetName
        {
            get { return _playerDataSet.Name; }
            set { _playerDataSet.Name = value; }
        }
        
        /// <summary>
        /// 맵의 Player에 해당하는 DataSet을 설정.
        /// </summary>
        public Character.CharacterDataSet PlayerDataSet
        {
            get { return _playerDataSet; }
            set { _playerDataSet = value; }
        }
        /// <summary>
        /// 몬스터를 가로막는 벽의 위치.
        /// </summary>
        public List<Vector2> MonsterBlockPositions
        {
            get { return _monsterBlockPositions; }
            set { _monsterBlockPositions = value; }
        }

        [ContentSerializerIgnore]
        public Dictionary<String, Substance> Substances
        {
            get { return _substancesStack.Peek(); }
        }

        /// <summary>
        /// ResourceManager에서 Map을 부를 때 해당 Map의 원본을 Clone해서 줌.
        /// 변하지 않을 것이라 가정할 수 있는 각종 Substances의 DataSet들과 AdditionalGeomverts같은 것은 그냥 shallow copy함.
        /// 제대로 사용하기 위해서는 Clone 후 BindDatas를 불러야 함.
        /// Map을 Run-time중에서 게임에서 부르기 위해서는 
        /// 1. ResourceManager.Load()를 이용하여 Map을 부른다. (Load함수 내부에서는 Clone으로 처리)
        /// 2. Map.BindDatas()를 호출 이 함수 안에서는 각 Substance오브젝트에 대해 먼저 BindSimulator()를 실행하고
        ///    SetData()를 통해 Map에서 세팅한 데이터로 각 Substance 오브젝트의 데이터를 설정한다.
        ///    그리고 각 레이어에 맞게 Map에 해당 Substance를 더한다.
        /// </summary>
        /// <returns></returns>
        public object Clone()
        {
            Map m = new Map();
            m.Name = this.Name;
            m.CharacterDatas = this.CharacterDatas;
            m.TerrainDatas = this.TerrainDatas;
            m.ItemDatas = this.ItemDatas;
            m.PlayerAssetName = this.PlayerAssetName;
            m.PlayerDataSet = this.PlayerDataSet;
            m.MonsterBlockPositions = this.MonsterBlockPositions;
            m.BgAssetName = this.BgAssetName;
            m.MapSize = this.MapSize;
            return m;
        }


        #endregion

        #region Processor
        public class MapReader : ContentTypeReader<Map>
        {
            protected override Map Read(ContentReader input, Map existingInstance)
            {
                int count; 
                Map map = new Map();
                map.Name = input.ReadString();
                map.MapSize = new Rectangle(input.ReadInt32(), input.ReadInt32(), input.ReadInt32(), input.ReadInt32());
                map.BgAssetName = input.ReadString();
                map.PlayerAssetName = input.ReadString();
                count = input.ReadInt32();
                map.CharacterDatas = new List<Character.CharacterDataSet>(count);
                for (int i = 0 ; i < count; ++i)
                {
                    Character.CharacterDataSet ds = new Character.CharacterDataSet();
                    ds.Deserialize(input);
                    map.CharacterDatas.Add(ds);
                }
                count = input.ReadInt32();
                map.TerrainDatas = new List<Terrain.TerrainDataSet>(count);
                for (int i = 0; i < count; ++i)
                {
                    Terrain.TerrainDataSet ds = new Terrain.TerrainDataSet();
                    ds.Deserialize(input);
                    map.TerrainDatas.Add(ds);
                }
                count = input.ReadInt32();
                map.ItemDatas = new List<Item.ItemDataSet>(count);
                for (int i = 0; i < count; ++i)
                {
                    Item.ItemDataSet ds = new Item.ItemDataSet();
                    ds.Deserialize(input);
                    map.ItemDatas.Add(ds);
                }
                count = input.ReadInt32();
                map.MonsterBlockPositions = new List<Vector2>(count);
                for (int i = 0; i < count; ++i)
                {
                    map.MonsterBlockPositions.Add(new Vector2(input.ReadSingle(), input.ReadSingle()));
                }
                map.PlayerDataSet.Deserialize(input);
                return map;
            }
        }

        [ContentTypeWriter]
        public class MapWriter : ContentTypeWriter<Map>
        {
            protected override void Write(ContentWriter output, Map value)
            {
                output.Write(value.Name);
                //2009. 8. 29 맵 크기와 기본 ckeckpoint 추가.
                output.Write(value.MapSize.X);
                output.Write(value.MapSize.Y);
                output.Write(value.MapSize.Width);
                output.Write(value.MapSize.Height);
                output.Write(value.BgAssetName);                

                output.Write(value.PlayerAssetName);
                output.Write(value.CharacterDatas.Count);
                for (int i = 0; i < value.CharacterDatas.Count; ++i)
                    value.CharacterDatas[i].Serialize(output);
                output.Write(value.TerrainDatas.Count);
                for (int i = 0; i < value.TerrainDatas.Count; ++i)
                    value.TerrainDatas[i].Serialize(output);
                output.Write(value.ItemDatas.Count);
                for (int i = 0; i < value.ItemDatas.Count; ++i)
                    value.ItemDatas[i].Serialize(output);
                output.Write(value.MonsterBlockPositions.Count);
                for (int i = 0; i < value.MonsterBlockPositions.Count; ++i)
                {
                    output.Write(value.MonsterBlockPositions[i].X);
                    output.Write(value.MonsterBlockPositions[i].Y);
                }
                value.PlayerDataSet.Serialize(output);
            }
            public override string GetRuntimeReader(TargetPlatform targetPlatform)
            {
                return typeof(Map.MapReader).AssemblyQualifiedName;
            }
            public override string GetRuntimeType(TargetPlatform targetPlatform)
            {
                return typeof(Map).AssemblyQualifiedName;
            }
        }
        #endregion
    }
}
